﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using static Microsoft.Win32.NativeMethods;

namespace Microsoft.Win32
{
	/// <summary>
	/// Attributes that can be retrieved on an item (file or folder) or set of items using <see cref="ShellItem.Attributes"/>.
	/// </summary>
	[Flags]
	public enum ShellItemAttribute : uint
	{
		/// <summary>The specified items can be copied.</summary>
		CanCopy = 0x00000001,

		/// <summary>The specified items can be moved.</summary>
		CanMove = 0x00000002,

		/// <summary>Shortcuts can be created for the specified items. If a namespace extension returns this attribute, a Create Shortcut entry with a default handler is added to the shortcut menu that is displayed during drag-and-drop operations. The extension can also implement its own handler for the link verb in place of the default. If the extension does so, it is responsible for creating the shortcut. A Create Shortcut item is also added to the Windows Explorer File menu and to normal shortcut menus. If the item is selected, your application's IContextMenu::InvokeCommand method is invoked with the lpVerb member of the CMINVOKECOMMANDINFO structure set to link. Your application is responsible for creating the link.</summary>
		CanLink = 0x00000004,

		/// <summary>The specified items can be bound to an IStorage object throughIShellFolder::BindToObject. For more information about namespace manipulation capabilities, see IStorage.</summary>
		Storage = 0x00000008,

		/// <summary>The specified items can be renamed. Note that this value is essentially a suggestion; not all namespace clients allow items to be renamed. However, those that do must have this attribute set.</summary>
		CanRename = 0x00000010,

		/// <summary>The specified items can be deleted.</summary>
		CanDelete = 0x00000020,

		/// <summary>The specified items have property sheets.</summary>
		HasPropSheet = 0x00000040,

		/// <summary>The specified items are drop targets.</summary>
		DropTarget = 0x00000100,

		/// <summary>This flag is a mask for the capability attributes: CANCOPY, CANMOVE, CANLINK, CANRENAME, CANDELETE, HASPROPSHEET, and DROPTARGET. Callers normally do not use this value.</summary>
		CapabilityMask = 0x00000177,

		/// <summary>Windows 7 and later. The specified items are system items.</summary>
		System = 0x00001000,

		/// <summary>The specified items are encrypted and might require special presentation.</summary>
		Encrypted = 0x00002000,

		/// <summary>Accessing the item (through IStream or other storage interfaces) is expected to be a slow operation. Applications should avoid accessing items flagged with ISSLOW. Note: Opening a stream for an item is generally a slow operation at all times. ISSLOW indicates that it is expected to be especially slow, for example in the case of slow network connections or offline (FILE_ATTRIBUTE_OFFLINE) files. However, querying ISSLOW is itself a slow operation. Applications should query ISSLOW only on a background thread. An alternate method, such as retrieving the PKEY_FileAttributes property and testing for FILE_ATTRIBUTE_OFFLINE, could be used in place of a method call that involves ISSLOW.</summary>
		IsSlow = 0x00004000,

		/// <summary>The specified items are shown as dimmed and unavailable to the user.</summary>
		Ghosted = 0x00008000,

		/// <summary>The specified items are shortcuts.</summary>
		Link = 0x00010000,

		/// <summary>The specified objects are shared.</summary>
		Share = 0x00020000,

		/// <summary>The specified items are read-only. In the case of folders, this means that new items cannot be created in those folders. This should not be confused with the behavior specified by the FILE_ATTRIBUTE_READONLY flag retrieved by IColumnProvider::GetItemData in a SHCOLUMNDATAstructure. FILE_ATTRIBUTE_READONLY has no meaning for Win32 file system folders.</summary>
		ReadOnly = 0x00040000,

		/// <summary>The item is hidden and should not be displayed unless the Show hidden files and folders option is enabled in Folder Settings.</summary>
		Hidden = 0x00080000,

		/// <summary>Do not use.</summary>
		DisplayAttrMask = 0x000FC000,

		/// <summary>The items are nonenumerated items and should be hidden. They are not returned through an enumerator such as that created by theIShellFolder::EnumObjects method.</summary>
		NonEnumerated = 0x00100000,

		/// <summary>The items contain new content, as defined by the particular application.</summary>
		NewContent = 0x00200000,

		/// <summary>Not supported.</summary>
		CanMoniker = 0x00400000,

		/// <summary>Not supported.</summary>
		HasStorage = 0x00400000,

		/// <summary>Indicates that the item has a stream associated with it. That stream can be accessed through a call to IShellFolder::BindToObject orIShellItem::BindToHandler with IID_IStream in the riid parameter.</summary>
		Stream = 0x00400000,

		/// <summary>Children of this item are accessible through IStream or IStorage. Those children are flagged with STORAGE or STREAM.</summary>
		StorageAncestor = 0x00800000,

		/// <summary>When specified as input, VALIDATE instructs the folder to validate that the items contained in a folder or Shell item array exist. If one or more of those items do not exist, IShellFolder::GetAttributesOf and IShellItemArray::GetAttributes return a failure code. This flag is never returned as an [out] value. When used with the file system folder, VALIDATE instructs the folder to discard cached properties retrieved by clients of IShellFolder2::GetDetailsEx that might have accumulated for the specified items.</summary>
		Validate = 0x01000000,

		/// <summary>The specified items are on removable media or are themselves removable devices.</summary>
		Removable = 0x02000000,

		/// <summary>The specified items are compressed.</summary>
		Compressed = 0x04000000,

		/// <summary>The specified items can be hosted inside a web browser or Windows Explorer frame.</summary>
		Browsable = 0x08000000,

		/// <summary>The specified folders are either file system folders or contain at least one descendant (child, grandchild, or later) that is a file system (FILESYSTEM) folder.</summary>
		FileSysAncestor = 0x10000000,

		/// <summary>The specified items are folders. Some items can be flagged with both STREAM and FOLDER, such as a compressed file with a .zip file name extension. Some applications might include this flag when testing for items that are both files and containers.</summary>
		Folder = 0x20000000,

		/// <summary>The specified folders or files are part of the file system (that is, they are files, directories, or root directories). The parsed names of the items can be assumed to be valid Win32 file system paths. These paths can be either UNC or drive-letter based.</summary>
		FileSystem = 0x40000000,

		/// <summary>This flag is a mask for the storage capability attributes: STORAGE, LINK, READONLY, STREAM, STORAGEANCESTOR, FILESYSANCESTOR, FOLDER, and FILESYSTEM. Callers normally do not use this value.</summary>
		StorageCapMask = 0x70C50008,

		/// <summary>The specified folders have subfolders. The HASSUBFOLDER attribute is only advisory and might be returned by Shell folder implementations even if they do not contain subfolders. Note, however, that the converse—failing to return HASSUBFOLDER—definitively states that the folder objects do not have subfolders. Returning HASSUBFOLDER is recommended whenever a significant amount of time is required to determine whether any subfolders exist. For example, the Shell always returns HASSUBFOLDER when a folder is located on a network drive.</summary>
		HasSubfolder = 0x80000000,

		/// <summary>This flag is a mask for content attributes, at present only HASSUBFOLDER. Callers normally do not use this value.</summary>
		ContentsMask = 0x80000000,

		/// <summary>Mask used by the PKEY_SFGAOFlags property to determine attributes that are considered to cause slow calculations or lack context: ISSLOW, READONLY, HASSUBFOLDER, and VALIDATE. Callers normally do not use this value.</summary>
		PKEYMask = 0x81044000,
	}

	/// <summary>
	/// Requests the form of an item's display name to retrieve through <see cref="ShellItem.GetDisplayName"/>.
	/// </summary>
	public enum ShellItemDisplayString : uint
	{
		/// <summary>Returns the display name relative to the parent folder. In UI this name is generally ideal for display to the user.</summary>
		NormalDisplay = 0x00000000,

		/// <summary>Returns the parsing name relative to the parent folder. This name is not suitable for use in UI.</summary>
		ParentRelativeParsing = 0x80018001,

		/// <summary>Returns the parsing name relative to the desktop. This name is not suitable for use in UI.</summary>
		DesktopAbsoluteParsing = 0x80028000,

		/// <summary>Returns the editing name relative to the parent folder. In UI this name is suitable for display to the user.</summary>
		ParentRelativeEditing = 0x80031001,

		/// <summary>Returns the editing name relative to the desktop. In UI this name is suitable for display to the user.</summary>
		DesktopAbsoluteEditing = 0x8004c000,

		/// <summary>Returns the item's file system path, if it has one. Only items that report SFGAO_FILESYSTEM have a file system path. When an item does not have a file system path, a call to IShellItem::GetDisplayName on that item will fail. In UI this name is suitable for display to the user in some cases, but note that it might not be specified for all items.</summary>
		FileSysPath = 0x80058000,

		/// <summary>Returns the item's URL, if it has one. Some items do not have a URL, and in those cases a call to IShellItem::GetDisplayName will fail. This name is suitable for display to the user in some cases, but note that it might not be specified for all items.</summary>
		Url = 0x80068000,

		/// <summary>Returns the path relative to the parent folder in a friendly format as displayed in an address bar. This name is suitable for display to the user.</summary>
		ParentRelativeForAddressBar = 0x8007c001,

		/// <summary>Returns the path relative to the parent folder.</summary>
		ParentRelative = 0x80080001,

		/// <summary>Introduced in Windows 8.</summary>
		ParentRelativeForUI = 0x80094001,
	}

	public class ShellItem : IComparable<ShellItem>, IDisposable
	{
		private IPropertyStore iprops;
		private IShellItem iShellItem;
		private IShellItem2 iShellItem2;

		private PropertyStoreValues values = null;

		private bool writable, slowProps;

		public ShellItem()
		{
		}

		private ShellItem(IShellItem si)
		{
			iShellItem = si;
			iShellItem2 = (IShellItem2)si;
		}

		//object BindToHandler(IBindCtx pbc, [In] ref Guid bhid, [In] ref Guid riid);

		public ShellItemAttribute Attributes => (ShellItemAttribute)iShellItem.GetAttributes((SFGAO)0xFFFFFFFF);
		public string DisplayName => iShellItem.GetDisplayName(SIGDN.NORMALDISPLAY);

		[DefaultValue(false)]
		public bool IncludeSlowProperties
		{
			get { return !slowProps; }
			set
			{
				if (value != slowProps)
				{
					slowProps = value;
					GetPropertyStore();
				}
			}
		}

		public ShellItem Parent => new ShellItem(iShellItem.GetParent());

		public PropertyStoreValues Properties
		{
			get { if (values == null) { values = new PropertyStoreValues(this); } return values; }
		}

		[DefaultValue(false)]
		public bool ReadWriteProperties
		{
			get { return !writable; }
			set
			{
				if (value != writable)
				{
					writable = value;
					GetPropertyStore();
				}
			}
		}

		public void Dispose()
		{
			if (iprops != null)
				Marshal.ReleaseComObject(iprops);
			if (iShellItem != null)
				Marshal.ReleaseComObject(iShellItem);
			if (iShellItem2 != null)
				Marshal.ReleaseComObject(iShellItem2);
		}

		public override bool Equals(object obj)
		{
			if (obj is ShellItem)
				return ((IComparable<ShellItem>)this).CompareTo((ShellItem)obj) == 0;
			if (obj is IShellItem)
				return iShellItem.Compare((IShellItem)obj, SICHINT.ALLFIELDS) == 0;
			return base.Equals(obj);
		}

		public Guid GetCLSID(Guid key)
		{
			CheckFor2();
			return iShellItem2.GetCLSID(new REFPROPERTYKEY(key));
		}

		public string GetDisplayName(ShellItemDisplayString option) => iShellItem.GetDisplayName((SIGDN)option);

		public override int GetHashCode() => GetDisplayName(ShellItemDisplayString.DesktopAbsoluteEditing).GetHashCode();

		public object GetProperty(Guid key)
		{
			CheckFor2();
			PROPVARIANT pv = new PROPVARIANT();
			iShellItem2.GetProperty(new REFPROPERTYKEY(key), pv);
			return pv.Value;
		}

		// void Update(IBindCtx pbc);
		int IComparable<ShellItem>.CompareTo(ShellItem other) => iShellItem.Compare(other.iShellItem, SICHINT.ALLFIELDS);

		public override string ToString() => DisplayName;

		// object GetPropertyDescriptionList(IntPtr keyType, [In] ref Guid riid);
		private void CheckFor2()
		{
			if (iShellItem2 == null)
				throw new InvalidOperationException("Unable to get access to this detail.");
		}

		private void GetPropertyStore()
		{
			CheckFor2();
			GPS gps = writable ? GPS.READWRITE : GPS.DEFAULT;
			if (slowProps)
				gps |= GPS.OPENSLOWITEM;
			if (iprops != null)
				Marshal.ReleaseComObject(iprops);
			iprops = iShellItem2.GetPropertyStore(gps, typeof(IPropertyStore).GUID) as IPropertyStore;
		}

		public class PropertyStoreValues : IEnumerable<object>
		{
			private readonly ShellItem outer;

			internal PropertyStoreValues(ShellItem outer)
			{
				this.outer = outer;
			}

			public int Length
			{
				get
				{
					if (outer.iprops == null)
						outer.GetPropertyStore();
					return (int)outer.iprops.GetCount();
				}
			}

			public object this[Guid key]
			{
				get
				{
					if (outer.iprops != null)
					{
						try
						{
							var pv = new PROPVARIANT();
							outer.iprops.GetValue(new REFPROPERTYKEY(key), pv);
							return pv.Value;
						}
						catch { }
					}
					return outer.GetProperty(key);
				}
				set
				{
					if (outer.iprops == null)
						outer.GetPropertyStore();
					outer.iprops.SetValue(new REFPROPERTYKEY(key), new PROPVARIANT(value));
				}
			}

			public void Commit()
			{
				if (outer.iprops == null)
					outer.GetPropertyStore();
				outer.iprops.Commit();
			}

			public System.Collections.Generic.IEnumerator<object> GetEnumerator()
			{
				if (outer.iprops == null)
					outer.GetPropertyStore();
				for (uint i = 0; i < Length; i++)
					yield return this[outer.iprops.GetAt(i).Key];
			}

			IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
		}

		private static class KnownKeys
		{
		}

		// object GetPropertyStoreWithCreateObject(GPS flags, object punkCreateObject, [In] ref Guid riid);

		// object GetPropertyStoreForKeys(IntPtr rgKeys, uint cKeys, GPS flags, [In] ref Guid riid);
	}
}